// TODO: CODE REVIEW & CLEANUP!
//
// TouchDropDownList.cs
//
// Implements TouchDropDownList and related types.
//

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using HomeUX.ClientControlSupport;
using HomeUX.Synchronization;
using SyncPathClass = HomeUX.Synchronization.SyncPath;
using System.ComponentModel;

namespace HomeUX.Controls
{

/// <summary>
/// 
/// </summary>
/// 
/// <remarks>
/// 
/// [See also: description in TouchListBox]
/// The drop-down list may be populated in three ways:
/// 1. Statically, by including
///    "&lt;TextBlock c:Item.Data="<i>item-data</i>"&gt;<i>item-label</i>&lt;/TextBlock&gt;"
///    elements under "&lt;c:TouchDropDownList&gt;".  Each <i>item-label</i> is the label that's displayed;
///    the corresponding <i>item-data</i> is the data associated with the item, interpreted according to
///    <r>ItemAction</r>.
/// 2. Dynamically, by adding items to the <n>Items</n> collection.  Each item must be a <r>TextBlock</r> object
///    whose <n>Text</n> property contains the item label.  Call Item.SetItemData--?? TODO to set the
///    data associated with the item.
/// 3. By binding to the application-wide SyncTree (using <r>SyncPath</r>) and specifying (via
///    <ar>ItemsConfigurationSyncPath</ar>) <r>SyncNode</r> whose child nodes contain list items.  This works the
///    same as for <r>TouchListBox</r> -- see that control for more information.
/// 
/// The following properties affect the drop-down:  DragZoneVisibility, ScrollButtonVisibility,
/// ScrollSliderVisibility, ListMargin, ItemStyle, ViewportMinWidth, ViewportMinHeight,
/// ViewportMaxWidth, ViewportMaxHeight, PageButtonVisibility.
///
/// See <r>TouchListBox</r> for information about how to size the drop-down using
/// ViewportMinWidth, ViewportMinHeight
/// ViewportMaxWidth, ViewportMaxHeight.
///
/// [TODO: clean up]
/// </remarks>
///
[TemplatePart(Name = LayoutRootName, Type = typeof(Panel))]
[TemplatePart(Name = TouchFieldName, Type = typeof(TouchField))]
[TemplatePart(Name = ValueTextBlockName, Type = typeof(TextBlock))]
[DebuggerDisplay("TouchDropDownList {Label}")]
public class TouchDropDownList : TouchSelector
{
    const string LayoutRootName = "LayoutRoot";
    const string TouchFieldName = "TouchField";
    const string ValueTextBlockName = "ValueTextBlock";

    Panel _layoutRoot;
    TouchField _touchField;
    TextBlock _valueTextBlock;

    /// <summary>
    /// Implements the connection between this control and the global SyncTree object, which in
    /// turn manages the connection to the server.
    /// </summary>
    SyncHelper _syncHelper;

    Dictionary<object, string> _mapItemValueToLabel = new Dictionary<object, string>();

    public static readonly DependencyProperty LabelProperty =
        DependencyProperty.Register("Label", typeof(object), typeof(TouchDropDownList), null);

    public static readonly DependencyProperty DropDownBorderThicknessProperty =
        DependencyProperty.Register("DropDownBorderThickness", typeof(Thickness), typeof(TouchDropDownList), null);

    public static readonly DependencyProperty IsPopupOpenProperty = DependencyProperty.Register("IsPopupOpen",
        typeof(bool), typeof(TouchDropDownList),
        new PropertyMetadata(new PropertyChangedCallback(TouchDropDownList.OnIsPopupOpenPropertyChanged)));

    public event EventHandler PopupOpened;
    public event EventHandler PopupClosed;
    public event IsWaitingChangedEventHandler IsWaitingChanged;
    public event ItemClickEventHandler ItemClick;
    public event EventHandler SelectedItemValueChanged;

    public object Label
    {
        get
        {
            return (object)GetValue(LabelProperty);
        }
        set
        {
            SetValue(LabelProperty, value);
        }
    }

    public Thickness DropDownBorderThickness
    {
        get
        {
            return (Thickness)GetValue(DropDownBorderThicknessProperty);
        }
        set
        {
            SetValue(DropDownBorderThicknessProperty, value);
        }
    }

    public bool IsPopupOpen
    {
        get
        {
            return (bool)GetValue(IsPopupOpenProperty);
        }
        set
        {
            SetValue(IsPopupOpenProperty, value);
        }
    }

    public TouchDropDownList()
    {
        DefaultStyleKey = typeof(TouchDropDownList);

        _syncHelper = new SyncHelper(this, SyncHelperFeatures.Configuration);

        // hook up <_syncHelper> events
        _syncHelper.IsOnlineChanged += _syncHelper_IsOnlineChanged;
        _syncHelper.IsWaitingChanged += _syncHelper_IsWaitingChanged;
        _syncHelper.PrimaryValueChanged += _syncHelper_PrimaryValueChanged;
        _syncHelper.ConfigurationChanged += _syncHelper_ConfigurationChanged;
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ControlHelper.GetRequiredTemplateChild(this, LayoutRootName, out _layoutRoot);
        ControlHelper.GetRequiredTemplateChild(this, TouchFieldName, out _touchField);
        ControlHelper.GetRequiredTemplateChild(this, ValueTextBlockName, out _valueTextBlock);

        GotFocus += TouchDropDownList_GotFocus;
        LostFocus += TouchDropDownList_LostFocus;
        KeyDown += TouchDropDownList_KeyDown;

        _layoutRoot.MouseLeftButtonDown += LayoutRoot_MouseLeftButtonDown;

        // check if this control is an orphan when Parent may change
        LayoutUpdated += (sender, e) => _syncHelper.CheckOrphanStatus(Parent);

        // in case this control is being created after the configuration node has already been
        // synchronized from the server, then if this control gets its items populated from the
        // configuration, do so now
        PopulateItemsFromConfiguration();

        // display the initial item value, if any
        object value = SelectedItemValue;
        if (value != null)
            OnSelectedItemValueChanged(null, value);
    }

    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {
        base.OnItemsChanged(e);

        switch (e.Action)
        {

        case NotifyCollectionChangedAction.Reset:

            // TODO: I don't think this is right -- I think I should enumerate all items here,
            // but I haven't seen this case happen yet
            _mapItemValueToLabel.Clear();
            break;

        case NotifyCollectionChangedAction.Add:
        case NotifyCollectionChangedAction.Replace:
        case NotifyCollectionChangedAction.Remove:

            ItemAction itemAction = ItemAction;
            object selectedItemValue = SelectedItemValue;
            if (e.OldItems != null)
            {
                foreach (object item in e.OldItems)
                {
                    object itemValue = Item.GetTypedValue(itemAction, item);
                    if (itemValue != null)
                    {
                        _mapItemValueToLabel.Remove(itemValue);

                        // if this item was being displayed as selected, blank the display since
                        // the item is being removed
                        if (Object.Equals(itemValue, selectedItemValue))
                            _valueTextBlock.Text = string.Empty;
                    }
                }
            }

            if (e.NewItems != null)
            {
                foreach (object item in e.NewItems)
                {
					object itemValue;
					string label;
					if (Item.GetTypedValueAndLabel(itemAction, item, out itemValue, out label))
					{
						_mapItemValueToLabel[itemValue] = label;

                        // if this item is supposed to be displayed as selected, do so (if possible)
                        if (Object.Equals(itemValue, selectedItemValue))
                        {
                            if (_valueTextBlock != null) // i.e. if OnApplyTemplate was called
                                _valueTextBlock.Text = label;
                        }
                    }
                }
            }

            break;
        }
    }

    protected override void OnSelectedItemValueChanged(object oldValue, object newValue)
    {
        if (_layoutRoot != null) // i.e. if OnApplyTemplate was called already
        {
            // ensure that <newValue> is valid for the ItemAction of this list box
            if ((newValue != null) &&
                (Item.TypedValueToUntypedValue(ItemAction, newValue) == null))
            {
                SelectedItemValue = null;
                throw new InvalidOperationException(String.Format(
                    "TouchDropDownList.SelectedItemValue cannot be set to a value of type \"{0}\" when ItemAction is \"{1}\"",
                    newValue.GetType(), ItemAction));
            }

            //zz

            // set <label> to the label to display that corresponds to <newValue>
            string label;
            if (newValue != null)
            {
                if (!_mapItemValueToLabel.TryGetValue(newValue, out label))
                    label = String.Empty;
            }
            else
                label = String.Empty;

            // display <label>
            _valueTextBlock.Text = label;

            // inform the application
            if (SelectedItemValueChanged != null)
                SelectedItemValueChanged(this, EventArgs.Empty);
        }
    }

    /// <summary>
    /// Called when <r>_syncHelper</r>.<s>SyncHelper.PrimaryNode</s>.<s>SyncNodeBase.Value</s> has changed -- for example, if another client has
    /// changed the value.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_PrimaryValueChanged(object sender, ValueChangedEventArgs e)
    {
        // ignore this event if the change originated from this control
        if (e.Originator == this)
            return;

        // select (i.e. light up) the list box item that has a ItemData value as specified
        // by <_syncHelper.PrimaryNode.Value>
        SelectedItemValue = _syncHelper.PrimaryNode.Value;
    }

    /// <summary>
    /// Called when <r>_syncHelper</r>.<s>SyncNode.IsWaiting</s> has changed.  This occurs when
    /// <r>_syncHelper</r>.<s>SyncHelper.PrimaryNode</s>.<s>SyncNodeBase.Value</s> is changed on this client, but the change has not yet been
    /// reflected on the server.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_IsWaitingChanged(object sender, EventArgs e)
    {
        // show or hide the wait icon (e.g. hourglass)
        IsWaiting = _syncHelper.IsWaiting;
    }

    /// <summary>
    /// Called when <r>_syncHelper</r>.<s>SyncHelper.IsOnline</s> has changed.  This occurs when the connection to
    /// the server has been broken or reestablished.  This also occurs when
    /// <r>_syncHelper</r>.<s>SyncHelper.PrimaryNode</s>.<s>SyncNodeBase.Value</s> becomes <n>null</n> (indicating, for example, that the device
    /// associated with that value is offline) or becomes non-<n>null</n>.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_IsOnlineChanged(object sender, EventArgs e)
    {
        // enable or disable the control
        IsEnabled = _syncHelper.IsOnline;
    }

    /// <summary>
    /// Called when a descendant of <r>_syncHelper</r>.<s>SyncHelper.ConfigurationNode</s> has changed -- for
    /// example, if another client has change the configuration information.
    /// </summary>
    ///
    /// <param name="sender">The sender of the event.</param>
    ///
    /// <param name="e">Event arguments.</param>
    ///
    void _syncHelper_ConfigurationChanged(object sender, DescendantChangedEventArgs e)
    {
        // ignore this event if the change originated from this control
        if (e.Originator == this)
            return;

        // load items from the configuration node
        PopulateItemsFromConfiguration();
    }

    /// <summary>
    /// Loads this controls items from its configuration node, if this control's list of items is
    /// bound to the SyncTree.
    /// </summary>
    ///
    void PopulateItemsFromConfiguration()
    {
        // do nothing if the list of items is not bound to the SyncTree
        if (ItemsConfigurationSyncPath == null)
            return;

        // do nothing if the configuration information is not currently available
        if (!_syncHelper.CheckAccess(SyncHelperFeatures.Configuration))
            return;

        // do nothing in design mode
        if (DesignerProperties.IsInDesignTool)
            return;

        // set <configurationSyncPath> to the path to the SyncNode whose children contain the items
        // to display, or null if the list box items aren't bound to the SyncTree
        SyncPath configurationSyncPath = SyncPathClass.FromString(ItemsConfigurationSyncPath);
        if (configurationSyncPath == null)
            return;

        // clear <Items>, then add the names of the children of <configurationSyncPath>
        Items.Clear();
        IEnumerable<SyncNodeBase> children;
        try
        {
            children = _syncHelper.ConfigurationNode[configurationSyncPath].Children;
        }
        catch (KeyNotFoundException)
        {
            // <configurationSyncPath> is invalid
            return;
        }
        foreach (SyncNode itemNode in children)
        {
            // ignore a SyncNode that has no value, since that's the convention that TouchListBox
            // uses for marking a previously-synchronized SyncNode as being deleted
            if (itemNode.Value == null)
                continue;

			// set <item> to a new TextBlock whose Text property is the label for the new item
			// (determined by the <itemNode.Value>) and attach a Item.Data
			// property with the item data
            var item = new TextBlock()
            {
                Text = itemNode.Value.ToString()
            };
            Item.SetUntypedValue(item, itemNode.Name);
            //item.SetValue(Item.DataProperty, itemNode.Name);

			// add <item> to the collection of items
            Items.Add(item);
        }
    }

    void TouchDropDownList_GotFocus(object sender, RoutedEventArgs e)
    {
        _touchField.DisplayAsFocused = true;
    }

    void TouchDropDownList_LostFocus(object sender, RoutedEventArgs e)
    {
        _touchField.DisplayAsFocused = false;
    }

    void TouchDropDownList_KeyDown(object sender, KeyEventArgs e)
    {
        if (!e.Handled)
        {
            e.Handled = OnKeyDown(e.Key);
        }
    }

    internal bool OnKeyDown(Key key)
    {
        switch (key)
        {

        case Key.Enter:
        case Key.Space:

            IsPopupOpen = !IsPopupOpen;
            break;

        default:

			return false;
        }

        return true;
    }

    TouchDropDownListPopup _popup;

    void LayoutRoot_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        IsPopupOpen = !IsPopupOpen;
    }

    void OnPopupClosed()
    {
		_popup = null;
		IsPopupOpen = false;
        _touchField.IsLit = false;
        if (PopupClosed != null)
            PopupClosed(this, EventArgs.Empty);
    }

    void OnIsPopupOpenPropertyChanged(bool isPopupOpen)
    {
        if (isPopupOpen)
        {
			if (_popup == null)
			{
                _touchField.IsLit = true;
                _touchField.UpdateLayout(); // TODO: why isn't this forcing immediate update?

                _popup = new TouchDropDownListPopup(this)
                {
                    Background = this.Background
                };
                ClientUtil.ShowDialog(this, _popup, new DialogSettings()
                {
                    PeekThrough = this,
                    OnClose = OnPopupClosed
                });
                if (PopupOpened != null)
                    PopupOpened(this, EventArgs.Empty);
			}
        }
        else
		{
			if (_popup != null)
			{
				// This horrible, horrible HACKHACK is to work around the following bug:
				//
				// Repro: open a drop-down list box, quickly select an item, and quickly repeat
				// this operation over and over until you get this:
                //     
                //     An unhandled exception ('Unhandled Error in Silverlight Application')
                //     Code: 4004
                //     Category: ManagedRuntimeError
                //     Message: System.ArgumentException: Value does not fall within the expected
				//     range.
                //     
				// This hack seems to reduce the odds of the bug happening (though I've still seen
                // it happen even with this workaround).  No idea why.
                //
                GC.Collect();
                System.Threading.Thread.Sleep(100);

                ClientUtil.CloseDialog(_popup, true);
				_popup = null;
            }
		}
    }

    static void OnIsPopupOpenPropertyChanged(DependencyObject d,
		DependencyPropertyChangedEventArgs e)
    {
        var target = (TouchDropDownList)d;
        target.OnIsPopupOpenPropertyChanged((bool)e.NewValue);
    }

    protected override void OnIsWaitingChanged(bool oldValue, bool newValue)
    {
        if (_touchField != null) // i.e. if OnApplyTemplate was called
        {
            _touchField.IsWaiting = newValue;

            // fire the IsWaitingChanged event
            if (IsWaitingChanged != null)
            {
                IsWaitingChanged(this, new IsWaitingChangedEventArgs()
                {
                    IsWaiting = (bool)newValue
                });
            }
        }
    }

    protected override void OnSyncPathChanged(string oldValue, string newValue)
    {
        _syncHelper.SyncPath = SyncPathClass.FromString(newValue);
    }

    protected override void OnConfigurationSyncPathChanged(string oldValue, string newValue)
    {
        _syncHelper.ConfigurationSyncPath = SyncPathClass.FromString(newValue);
    }

	internal void OnItemClick(ItemClickEventArgs e)
	{
        // perform the action specified by <ItemAction> on this control and <ItemData> on
        // the clicked-on item
        string data = Item.GetUntypedValue(e.Item);
        if (data != null)
            _syncHelper.PerformItemAction(ItemAction, data, this);

        // fire the ItemClick event
        if (ItemClick != null)
        {
            ItemClick(this, new ItemClickEventArgs()
            {
                Item = e.Item,
                ItemValue = e.ItemValue
            });
        }
    }
}

[TemplatePart(Name = LayoutRootName, Type = typeof(Panel))]
[TemplatePart(Name = TouchListBoxName, Type = typeof(TouchListBox))]
public class TouchDropDownListPopup : Control, IControlHelperConnector, INotifyDialog
{
    const string LayoutRootName = "LayoutRoot";
    const string TouchListBoxName = "TouchListBox";

    Panel _layoutRoot;

    TouchDropDownList _owner;
    TouchListBox _touchListBox;

    bool _dialogIsAbovePeekThroughControl;

    public TouchDropDownListPopup(TouchDropDownList owner)
    {
        DefaultStyleKey = typeof(TouchDropDownListPopup);
        _owner = owner;
        Loaded += TouchDropDownListPopup_Loaded;
        KeyDown += TouchDropDownListPopup_KeyDown;
    }

    void TouchDropDownListPopup_KeyDown(object sender, KeyEventArgs e)
    {
        if (!e.Handled)
        {
            if (e.Key != Key.Space)
                e.Handled = _owner.OnKeyDown(e.Key);
        }
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ControlHelper.GetRequiredTemplateChild(this, LayoutRootName, out _layoutRoot);
        ControlHelper.GetRequiredTemplateChild(this, TouchListBoxName, out _touchListBox);

		_touchListBox.DragZoneVisibility = _owner.DragZoneVisibility;
		_touchListBox.ScrollButtonVisibility = _owner.ScrollButtonVisibility;
		_touchListBox.ScrollSliderVisibility = _owner.ScrollSliderVisibility;
		_touchListBox.ListMargin = _owner.ListMargin;
		_touchListBox.ItemStyle = _owner.ItemStyle;
		_touchListBox.ViewportMinWidth = _owner.ViewportMinWidth;
		_touchListBox.ViewportMinHeight = _owner.ViewportMinHeight;
        _touchListBox.ViewportMaxWidth = _owner.ViewportMaxWidth;
        _touchListBox.ViewportMaxHeight = _owner.ViewportMaxHeight;
        _touchListBox.PageButtonVisibility = _owner.PageButtonVisibility;
		_touchListBox.ItemAction = _owner.ItemAction;
        _touchListBox.BorderThickness = _owner.DropDownBorderThickness;

		_touchListBox.SelectedItemValue = _owner.SelectedItemValue;

        // IMPORTANT: to avoid memory leaks, remove the event handlers below in OnDialogClose
        _touchListBox.SelectedItemValueChanged += _touchListBox_SelectedItemValueChanged;
        _touchListBox.ItemClick += _touchListBox_ItemClick;
        // IMPORTANT: to avoid memory leaks, remove the event handlers above in OnDialogClose

        //ItemAction itemAction = _owner.ItemAction;
		foreach (object item in _owner.Items)
		{
            string data, label;
            if (Item.GetUntypedValueAndLabel(item, out data, out label))
            {
                var itemClone = new TextBlock()
                {
                    Text = label
                };
                if (_owner.ItemStyle != null)
                    itemClone.Style = _owner.ItemStyle;
                Item.SetUntypedValue(itemClone, data);
                _touchListBox.Items.Add(itemClone);
            }
		}
    }

    void _touchListBox_SelectedItemValueChanged(object sender, EventArgs e)
    {
        _owner.SelectedItemValue = _touchListBox.SelectedItemValue;
    }

    void _touchListBox_ItemClick(object sender, ItemClickEventArgs e)
    {
        _owner.OnItemClick(e);
        _owner.IsPopupOpen = false;
    }

    void INotifyDialog.OnDialogPosition(bool isAbovePeekThroughControl)
    {
        _dialogIsAbovePeekThroughControl = isAbovePeekThroughControl;
    }

    void INotifyDialog.OnDialogClose()
    {
        // remove any other references to this object (to avoid blocking garbage collection)
        _touchListBox.SelectedItemValueChanged -= _touchListBox_SelectedItemValueChanged;
        _touchListBox.ItemClick -= _touchListBox_ItemClick;
    }

    void TouchDropDownListPopup_Loaded(object sender, RoutedEventArgs e)
    {
		// set initial focus
        Dispatcher.BeginInvoke(delegate { _touchListBox.Focus(); });
    }

    Control IControlHelperConnector.GetControl()
    {
        return this;
    }

    DependencyObject IControlHelperConnector.GetTemplateChild(string childName)
    {
        return GetTemplateChild(childName);
    }
}

}

